//
//  PMView.m
//  PezMonkeyTest
//
//  Created by Spence DiNicolantonio on 11/3/09.
//  Copyright 2009 Necrosoft. All rights reserved.
//

#import "PMView.h"
#import <GLUT/GLUT.h>
#import "PezMonkey.h"
#import "PZKeyCodes.h"

// demos
#import "LogoSmash.h"
#import "Simple.h"
#import "PyramidStack.h"
#import "Plink.h"
#import "Tumble.h"
#import "PyramidTopple.h"
#import "Bounce.h"
#import "Planet.h"
#import "Springies.h"
#import "Pump.h"
#import "TheoJansen.h"
#import "WalkBot.h"
#import "Unsafe.h"
#import "Query.h"
#import "OneWay.h"
#import "Player.h"
#import "Sensors.h"
#import "Constraints.h"

#define kFramerate (1.0 / 60.0)
#define kMouseShapeRadius 20
#define kMouseJointMaxForce 1000000
#define kWreckingBallCollisionType 666
#define kWreckingBallMaxForce PMInfinity


@implementation PMView

@synthesize demo;
@synthesize mouseBody;
@synthesize mouseShape;
@synthesize mouseJoint;


#pragma mark -
#pragma mark Init

- (id)initWithCoder:(NSCoder *)coder {
	if (![super initWithCoder:coder])
		return nil;
	
	// initialize PezMonkey
	[PezMonkey initialize];
	
	// create mouse body and joint
	self.mouseBody = [PMBody staticBody];
	self.mouseJoint = nil;
	self.mouseShape = nil;
	
	// set demo
	self.demo = [[[LogoSmash alloc] init] autorelease];
	[self startDemo];
	
	// create timer
	[NSTimer scheduledTimerWithTimeInterval:kFramerate 
									 target:self 
								   selector:@selector(timerFire:) 
								   userInfo:nil 
									repeats:YES];
	return self;
}


#pragma mark -
#pragma mark Timer

- (void)timerFire:(NSString *)timer {
//	// calculate time since last fire
//	static NSTimeInterval lastTime = 0.0;
//	if (lastTime == 0.0)
//		lastTime = [NSDate timeIntervalSinceReferenceDate];
//	NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
//	NSTimeInterval deltaT = currentTime - lastTime;
//	lastTime = currentTime;
	
	// step
	[demo.space step:kFramerate];
	[demo update:kFramerate];
	
	// request redraw
	[self setNeedsDisplay:YES];
}


#pragma mark -
#pragma mark OpenGL

- (void)prepareOpenGL {
	//NSLog(@"Preparing OpenGL");
	
	glClearColor(1.0, 1.0, 1.0, 0.0);
	glEnable(GL_LINE_SMOOTH);
	glEnable(GL_POINT_SMOOTH);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glHint(GL_LINE_SMOOTH_HINT, GL_DONT_CARE);
	glHint(GL_POINT_SMOOTH_HINT, GL_DONT_CARE);
	//glPointSize(1.0);
	//glLineWidth(1.5);
	glEnableClientState(GL_VERTEX_ARRAY);
}

- (void)reshape {
	//NSLog(@"Reshaping");
	
	// Convert up to window space, which is in pixel units
	NSRect baseRect = [self bounds];//[self convertRectToBase:[self bounds]];
	
	// Now the result is glViewport()-compatible
	glViewport(0, 0, baseRect.size.width, baseRect.size.height);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-320.0, 320.0, -240.0, 240.0, -1.0, 1.0);
	glTranslatef(0.5, 0.5, 0.0);
}

- (void)drawRect:(NSRect)rect {
	//NSLog(@"Drawing");
	
	// Clear the background
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	
	// Reset the coordinate system
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	
	// Draw
	[demo draw];
	
	// Flush to screen
	glFlush();
	glSwapAPPLE();
}


#pragma mark -
#pragma mark Set Demo

- (void)startDemo:(NSString *)letter {
	
	// select demo
	PMDemo *newDemo = nil;
	switch ([letter characterAtIndex:0]) {
		case 'a':
			newDemo = [[[LogoSmash alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'b':
			newDemo = [[[Simple alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'c':
			newDemo = [[[PyramidStack alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'd':
			newDemo = [[[Plink alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'e':
			newDemo = [[[Tumble alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'f':
			newDemo = [[[PyramidTopple alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'g':
			newDemo = [[[Bounce alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'h':
			newDemo = [[[Planet alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'i':
			newDemo = [[[Springies alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'j':
			newDemo = [[[Pump alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'k':
			newDemo = [[[TheoJansen alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'l':
			newDemo = [[[WalkBot alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'm':
			newDemo = [[[Unsafe alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'n':
			newDemo = [[[Query alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'o':
			newDemo = [[[OneWay alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'p':
			newDemo = [[[Player alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'q':
			newDemo = [[[Sensors alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		case 'r':
			newDemo = [[[Constraints alloc] initWithDemoOptions:demo.demoOptions] autorelease];
			break;
		default:
			break;
	}
	
	// set and start demo
	if (newDemo) {
		self.demo = newDemo;
		[demo start];
	}
}

- (void)startDemo {
	// reset demo
	[demo start];
	
	// reset mouse joint
	self.mouseJoint = nil;
}


#pragma mark -
#pragma mark Keyboard / Mouse

- (BOOL)acceptsFirstResponder {
	return YES;
}

- (void)keyDown:(NSEvent *)theEvent {
	
	// demo restart
	if (theEvent.keyCode == PZKeyCodeNumPadEnter ||
		theEvent.keyCode == PZKeyCodeReturn) {
		[demo start];
		return;
	}
	
	// toggles:
	NSUInteger keyCode = [theEvent keyCode];
	PMDemoOptions options = demo.demoOptions;
	switch (keyCode) {
		case PZKeyCode1:
			options.drawBodyPoints = !options.drawBodyPoints;
			demo.demoOptions = options;
			return;
		case PZKeyCode2:
			options.drawCollisions = !options.drawCollisions;
			demo.demoOptions = options;
			return;
		case PZKeyCode3:
			options.drawBoundingBoxes = !options.drawBoundingBoxes;
			demo.demoOptions = options;
			return;
		case PZKeyCode4:
			options.drawSpacialHash = !options.drawSpacialHash;
			demo.demoOptions = options;
			return;
	}
	
	// arrow keys
	switch (keyCode) {
		case PZKeyCodeLeftArrow:
			[demo setLeftArrow:YES];
			break;
		case PZKeyCodeRightArrow:
			[demo setRightArrow:YES];
			break;
		case PZKeyCodeUpArrow:
			[demo setUpArrow:YES];
			break;
		case PZKeyCodeDownArrow:
			[demo setDownArrow:YES];
			break;
		default:
			break;
	}
		
	[self startDemo:[theEvent characters]];
}

- (void)keyUp:(NSEvent *)theEvent {
	NSUInteger keyCode = [theEvent keyCode];
	
	// arrow keys
	switch (keyCode) {
		case PZKeyCodeLeftArrow:
			[demo setLeftArrow:NO];
			break;
		case PZKeyCodeRightArrow:
			[demo setRightArrow:NO];
			break;
		case PZKeyCodeUpArrow:
			[demo setUpArrow:NO];
			break;
		case PZKeyCodeDownArrow:
			[demo setDownArrow:NO];
			break;
		default:
			break;
	}
}

- (void)mouseDown:(NSEvent *)theEvent {
	// update mousePoint and mouseBody's position
	PMVect point = [self mouseToSpace:[theEvent locationInWindow]];
	point.y *= -1;
	mousePoint = point;
	mouseBody.position = mousePoint;
	
	// remove shape on double-click
	if ([theEvent clickCount] == 2) {
		for (PMShape *shape in [demo.space shapes])
			if ([shape containsPoint:mousePoint])
				[demo.space postStepRemoveShapeAndBody:shape];
		return;
	}
	
	// grab a shape
	PMShape *grabbedShape = [demo.space shapeAtPoint:mousePoint 
											  layers:(~PMLayerMaskNotGrabbable) 
											   group:PMCollisionGroupNone];
	
	// shape grabbed?
	BOOL grabbed = grabbedShape? YES : NO;
	
	// create "wrecking ball"
	if (!grabbedShape) {
		PMFloat mass = 1.0;
		PMFloat moment = PMMomentForCircle(mass, 0.0, kMouseShapeRadius, PMVectZero);
		
		// body
		PMBody *body = [PMBody bodyWithMass:mass 
									 moment:moment];
		[body setPosition:mousePoint];
		[demo.space addBody:body];
		
		// shape
		self.mouseShape = [PMCircleShape circleShapeWithBody:body 
													  radius:kMouseShapeRadius];
		[mouseShape setFriction:1.0];
		[mouseShape setElasticity:0.0];
		[mouseShape setCollisionType:kWreckingBallCollisionType];
		[demo.space addShape:mouseShape];
		grabbedShape = mouseShape;
	}
	
	// create joint for mouse
	self.mouseJoint = [PMPivotJoint pivotJointWithAnchor:PMVectZero 
												 onBodyA:mouseBody 
												  anchor:[grabbedShape.body convertPointToLocal:mousePoint] 
												 onBodyB:grabbedShape.body];
	
	if (grabbed)
		[mouseJoint setMaxForce:kMouseJointMaxForce];
	else
		[mouseJoint setMaxForce:kWreckingBallMaxForce];
	[mouseJoint setBiasCoef:0.15];
	[demo.space addConstraint:mouseJoint];
}

- (void)mouseUp:(NSEvent *)theEvent {
	// remove mouse joint from demo space
	if (mouseJoint) {
		[demo.space postStepRemoveConstraint:mouseJoint];
		self.mouseJoint = nil;
	} 
	
	// remove mouse shape from demo space
	if (mouseShape) {
		[demo.space postStepRemoveShapeAndBody:mouseShape];
		self.mouseShape = nil;
	}
}

- (void)mouseDragged:(NSEvent *)theEvent {
	// update mousePoint and mouseBody's position
	PMVect point = [self mouseToSpace:[theEvent locationInWindow]];
	point.y *= -1;
	mousePoint = point;
	mouseBody.position = mousePoint;
	
	// update demo mouse point
	demo.mousePoint = point;
}

- (void)mouseMoved:(NSEvent *)theEvent {
	PMVect point = [self mouseToSpace:[theEvent locationInWindow]];
	point.y *= -1;
	
	// update demo mouse point
	demo.mousePoint = point;
}

- (void)viewDidMoveToWindow {
	[self.window setAcceptsMouseMovedEvents:YES];
}

- (PMVect)mouseToSpace:(NSPoint)point {
	GLdouble model[16];
	glGetDoublev(GL_MODELVIEW_MATRIX, model);
	
	GLdouble proj[16];
	glGetDoublev(GL_PROJECTION_MATRIX, proj);
	
	GLint view[4];
	glGetIntegerv(GL_VIEWPORT, view);
	
	GLdouble mx, my, mz;
	gluUnProject(point.x, self.bounds.size.height - point.y, 0.0f, model, proj, view, &mx, &my, &mz);
	
	return PMVectMake(mx, my);
}
	

@end
